package com.societyGames.codeAlong.state.codeExercise.model
{
import com.societyGames.codeAlong.analytics.Analytics;
import com.societyGames.codeAlong.codeExercise.Compiler;
import com.societyGames.codeAlong.state.codeExercise.model.help.IExceptionHelp;
import com.societyGames.codeAlong.state.codeExercise.model.help.ISyntaxErrorHelp;
import com.societyGames.codeAlong.state.codeExercise.model.link.LinkHash;
import com.societyGames.codeAlong.state.codeExercise.model.testCase.TestCaseController;
import com.societyGames.codeAlong.user.PersistentUserData;
import com.societyGames.system.EventObject;
import com.societyGames.system.FileCache;

import flash.events.Event;
import flash.events.EventDispatcher;
import flash.utils.ByteArray;

//The information side an editor session.
public class EditorModel extends EventDispatcher //implements IServiceProvider
{
  public static const userCodeSourceFile:String = "Your Code";

  private var _syntaxErrorMessageEventObject:EventObject;	//SyntaxErrorMessageEvent: The user code had a syntax error.
  private var _exceptionEventObject:EventObject;		//ExceptionEvent: The user code had an exception.
  private var _unhandledExceptionEventObject:EventObject; //Event: User code had unhandled exception.
  private var _saveFailedEventObject:EventObject; //Event
  private var _userData:PersistentUserData;
  private var _moduleData:ModuleData; 	//Metadata around the module.
  private var _syntaxErrorsData:SyntaxErrorsData; //Syntax error help.
  private var _exceptionsData:ExceptionsData;		//Exceptions help.
  private var _compiler:Compiler;			//The preloaded compiler.
  private var _fileCache:FileCache;			//Shared file cache.
  private var _sharedData:SharedData;

  private var _codeOffset:int; 							//How much code is added by the module data, ie non-user code.
  private var _linkHash:LinkHash = new LinkHash();
  private var _testCaseController:TestCaseController;

  public function EditorModel(userData:PersistentUserData, moduleData:ModuleData, syntaxErrorsData:SyntaxErrorsData, exceptionsData:ExceptionsData, compiler:Compiler, fileCache:FileCache, sharedData:SharedData)
  {
    if (moduleData != null)
    {
      Analytics.view(moduleData);
    }

    this._syntaxErrorMessageEventObject = new EventObject(this, "SYNTAX_ERROR_MESSAGE", SyntaxErrorMessageEvent);
    this._exceptionEventObject = new EventObject(this, "EXCEPTION", ExceptionEvent);
    this._unhandledExceptionEventObject = new EventObject(this, "UNHANDLED_EXCEPTION");
    this._saveFailedEventObject = new EventObject(this, "SAVE_FAILED");

    this._userData = userData;
    this._moduleData = moduleData;
    this._syntaxErrorsData = syntaxErrorsData;
    this._exceptionsData = exceptionsData;
    this._compiler = compiler;
    this._fileCache = fileCache;
    this._sharedData = sharedData;

    //LinkHash
    this._linkHash.addLibrary(this._moduleData.libraryData);
    for (var functionIndex:int = 0; functionIndex < this._moduleData.functionIndex; functionIndex++)
    {
      this._linkHash.addFunction(this._sharedData.functionDatas[functionIndex]);
    }
    /*for (var classIndex:int = 0; classIndex < this._moduleData.classIndex; classIndex++)
    {
      this._linkHash.addClass(this._sharedData.classDatas[classIndex]);
    }*/
    for (var termIndex:int = 0; termIndex < this._moduleData.termIndex; termIndex++)
    {
      this._linkHash.addLinkable(this._sharedData.termDatas[termIndex]);
    }
    //TestCaseManager
    this._testCaseController = new TestCaseController(this._fileCache, moduleData.cases);
    this._testCaseController.selectedCaseStatusChangedEventObject.addEventListener(selectedTestCaseStatusChangedHandler, false, 0, true);
    this._testCaseController.testCasesSucceededEventObject.addEventListener(testCaseControllerCasesSucceededHandler, false, 0, true);
    this._testCaseController.exceptionEventObject.addEventListener(exceptionHandler, false, 0, true);
    this._testCaseController.unhandledExceptionEventObject.addEventListener(unhandledExceptionHandler, false, 0, true);
  }

  public function get testCaseController():TestCaseController
  {
    return this._testCaseController;
  }

  private function exceptionHandler(event:ExceptionEvent):void
  {
    Analytics.exception(_moduleData.id, this._testCaseController.currentTestCaseIndex, event.exception.error);
    var message:StackTraceMessage = event.exception.stackTraceMessage;
    if (message != null)
    {
      for each (var callMessage:StackTraceCallMessage in message.calls)
      {
        if (callMessage.sourceFile == EditorModel.userCodeSourceFile)
        {
          callMessage.lineNumber -= this._codeOffset;
        }
      }
    }
    this._exceptionEventObject.dispatchEvent(new ExceptionEvent(this._exceptionEventObject.type, event.exception));
  }

  private function unhandledExceptionHandler(event:Event):void
  {
    Analytics.unhandledException(_moduleData.id, this._testCaseController.currentTestCaseIndex);
    this._unhandledExceptionEventObject.dispatchEvent(new Event(this._unhandledExceptionEventObject.type));
  }

  //User code had syntax error
  public function get syntaxErrorMessageEventObject():EventObject //SyntaxErrorMessageEvent
  {
    return this._syntaxErrorMessageEventObject;
  }

  //User code had exception
  public function get exceptionEventObject():EventObject //ExceptionEvent
  {
    return this._exceptionEventObject;
  }

  //User code had exception
  public function get unhandledExceptionEventObject():EventObject //Event
  {
    return this._unhandledExceptionEventObject;
  }

  public function get saveFailedEventObject():EventObject //Event
  {
    return this._saveFailedEventObject;
  }

  public function get userData():PersistentUserData
  {
    return this._userData;
  }

  //Get the metadata associated with this module.
  public function get moduleData():ModuleData
  {
    return this._moduleData;
  }

  // The links this module is aware of.
  public function get linkHash():LinkHash
  {
    return this._linkHash;
  }

  //The terms associated with this moduleData and before.
  public function get sharedData():SharedData
  {
    return this._sharedData;
  }

  private function selectedTestCaseStatusChangedHandler(event:Event):void
  {
    switch (_testCaseController.selectedTestCase.testCaseStatus)
    {
      case TestCaseStatus.failed:
        Analytics.failTestCase(_moduleData.id, _testCaseController.currentTestCaseIndex);
        break;
      case TestCaseStatus.succeeded:
        Analytics.succeedTestCase(_moduleData.id, _testCaseController.currentTestCaseIndex);
        break;
    }
  }

  private function testCaseControllerCasesSucceededHandler(event:Event):void
  {
    if (this._moduleData.canComplete && !this._userData.completedExerciseIds.getValue(this._moduleData.id))
    {
      this._userData.completedExerciseIds.setValue(this._moduleData.id, true);
      this._userData.save();
      Analytics.succeedAllFirstTime(this._moduleData.id);
    }
    Analytics.succeedAll(this._moduleData.id);
  }

  public function execute(codeText:String):void
  {
    this._userData.currentCode = codeText;
    this._userData.saveCompletedEventObject.addEventListener(saveCompletedHandler, false, 0, true); //Don't want to keep this model alive.
    this._userData.saveFailedEventObject.addEventListener(saveFailedHandler, false, 0, true); //We don't care if the save failed.
    this._userData.save();
  }

  public function saveCompletedHandler(event:Event):void
  {
    this._userData.saveCompletedEventObject.removeEventListener(saveCompletedHandler); //Don't want to receive save messages from other people.
    this._userData.saveFailedEventObject.removeEventListener(saveCompletedHandler); //Don't want to receive save messages from other people.

    var codeText:String = this._userData.currentCode;
    //Append and postpend code.
    if (this._moduleData.codeBegin != null)
    {
      var codeBegin:String = addNewlineAtEnd(this._moduleData.codeBegin);
      this._codeOffset = countLines(codeBegin);
      codeText = codeBegin + codeText;
    }
    else
    {
      this._codeOffset = 0;
    }
    if (this._moduleData.codeEnd != null)
    {
      codeText = addNewlineAtEnd(codeText);
      codeText += this._moduleData.codeEnd;
    }

    //trace(codeText);
    Analytics.compile(this._moduleData);

    //Actually run it.
    try
    {
      var userCodeByteArray:ByteArray = this._compiler.compileSwf(codeText, userCodeSourceFile);
      this._testCaseController.execute(userCodeByteArray);
    }
    catch (syntaxError:SyntaxError)
    {
      Analytics.syntaxError(this._moduleData, syntaxError);

      var syntaxErrorMessage:SyntaxErrorMessage = SyntaxErrorMessage.fromSyntaxError(syntaxError);
      syntaxErrorMessage.line -= this._codeOffset;
      this._syntaxErrorMessageEventObject.dispatchEvent(new SyntaxErrorMessageEvent(this._syntaxErrorMessageEventObject.type, syntaxErrorMessage));
    }
  } //end function.

  private function addNewlineAtEnd(text:String):String
  {
    if (text.charAt(text.length - 1) != "\n")
    {
      return text + "\n";
    }
    return text;
  }

  private function countLines(text:String):int
  {
    var count:int = 0;
    var lastIndexOf:int = 0;
    while ((lastIndexOf = text.indexOf("\n", lastIndexOf)) != -1)
    {
      count++;
      lastIndexOf++;
    }
    return count;
  }

  private function saveFailedHandler(event:Event):void
  {
    this._saveFailedEventObject.dispatchEvent();
  }

  public function stop():void
  {
    this._testCaseController.stop();
  }

  public function getExerciseSyntaxErrorHelp(syntaxErrorMessage:SyntaxErrorMessage):Vector.<String>
  {
    var hyperTexts:Vector.<String> = new Vector.<String>();
    for each(var syntaxErrorHelp:ISyntaxErrorHelp in this._moduleData.syntaxErrorHelps)
    {
      var hyperText:String = syntaxErrorHelp.getHelp(syntaxErrorMessage);
      if (hyperText != null)
      {
        hyperTexts.push(hyperText);
      }
    }
    return hyperTexts;
  }

  public function getSpecificSyntaxErrorHelp(syntaxErrorMessage:SyntaxErrorMessage):String
  {
    return this._syntaxErrorsData.match(syntaxErrorMessage.message);
  }

  public function getExerciseExceptionHelp(exception:Exception):Vector.<String>
  {
    var hyperTexts:Vector.<String> = new Vector.<String>();
    for each(var exceptionHelp:IExceptionHelp in this._moduleData.exceptionHelps)
    {
      var hyperText:String = exceptionHelp.getHelp(exception);
      if (hyperText != null)
      {
        hyperTexts.push(hyperText);
      }
    }
    return hyperTexts;
  }

  public function getSpecificExceptionHelp(exception:Exception):String
  {
    return this._exceptionsData.match(exception.error.errorID);
  }

  public function closeCurrentLesson():void
  {
    this._userData.currentCode = null;
  }
}
}